/**
 * Copyright (c) 2018, vindell (https://github.com/vindell).
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.github.vindell.dbperms.jdbc.option;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale.Category;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import com.github.vindell.dbperms.jdbc.common.ClassHasher;

public class PermsOptionsRepository {
	
	 private final Map<String, Object> map = new HashMap<String, Object>();

	  private Set<DelayedOptionChange> delayedOptionChanges = new HashSet<DelayedOptionChange>();

	  private List<PermsOptionChangedListener> listeners = new ArrayList<PermsOptionChangedListener>();

	  /**
	   * Inidicator whether initialization has been completed. To prevent usage of not yet initialized
	   * properties.
	   */
	  private boolean initCompleted = false;

	  public void initCompleted() {
	    this.initCompleted = true;
	    fireDelayedOptionChanges();
	  }

	  public <T> boolean set(Class<T> type, String key, Object value) {
	    if (value == null) {
	      return false;
	    }

	    return setOrUnSet(type, key, value, null);
	  }
	  
	  public <T> boolean setOrUnSet(Class<T> type, String key, Object value, Object defaultValue) {
	    if (key == null || key.isEmpty()) {
	      throw new IllegalArgumentException("key can be neither null nor empty!");
	    }

	    if (value == null) {
	      value = defaultValue;
	    }
	    
	    if (value == null) {
	      setInternal(key, value);
	    } else {
	      setInternal(key, parse(type, value));  
	    }

	    return true;
	  }

	  <T> Object parse(Class<T> type, Object value) {
	    if (type.isAssignableFrom(Boolean.class)) {
	      return P6Util.isTrue(value.toString(), true);
	    } else if (type.isAssignableFrom(String.class)) {
	      return value.toString();
	    } else if (type.isAssignableFrom(Long.class)) {
	      return Long.parseLong(value.toString());
	    } else if (type.isAssignableFrom(Integer.class)) {
	      return Integer.parseInt(value.toString());
	    } else if (type.isAssignableFrom(Set.class)) {
	      throw new IllegalArgumentException("please call the setSet() method instead!");
	    } else if (type.isAssignableFrom(Collection.class) || type.isAssignableFrom(List.class)) {
	      throw new IllegalArgumentException("type not supported:" + type.getName());
	    } else if (type.isAssignableFrom(Pattern.class)) {
	      return Pattern.compile(value.toString());
	    } else {
//			if (type.isEnum()) {
//		    	// is sufficient for our use case where toString returns enum name
//		    	for (T enumConstant : type.getEnumConstants()) {
//		    	   if (enumConstant.toString().equalsIgnoreCase(value.toString())) {
//		    		   return enumConstant;
//		    	   }
//		    	}
//			}
	        	
	      Object instance;
	      try {
	        instance = P6Util.forName(value.toString()).newInstance();
	      } catch (Exception ex) {
	        // try one more hack to load the thing
	        try {
	          ClassLoader loader = ClassLoader.getSystemClassLoader();
	          instance = loader.loadClass(value.toString()).newInstance();
	        } catch (Exception e) {
	          System.err.println("Cannot instantiate " + value + ", even on second attempt. ");
	          e.printStackTrace(System.err);
	          return null;
	        }
	      }

	      try {
			T typedInstance = (T) instance;
	        return typedInstance;
	      } catch (ClassCastException e) {
	        System.err.println("Value " + value + ", is not of expected type. Error: " + e);
	        return null;
	      }
	    }
	  }

	  void setInternal(String key, Object value) {
	    final Object oldValue = map.put(key, value);

	    // propagate the changes
	    fireOptionChanged(key, oldValue, value);
	  }

	  @SuppressWarnings("unchecked")
	  public <T> boolean setSet(Class<T> type, String key, String csv) {
	    if (csv == null) {
	      return false;
	    }

	    final List<String> collection = P6Util.parseCSVList(csv);

	    if (collection == null) {
	      return false;
	    }

	    final Set<T> oldValue = getSet(type, key);
	    Set<T> newValue = null;

	    if (collection.isEmpty()) {
	    	map.remove(key);
		} else {
			if (type.equals(P6Factory.class)) {
		    	// for P6Factories the hashcode is computed based on class
		    	newValue = new CustomHashedHashSet<T>(new ClassHasher());
		    } else {
		    	newValue = new HashSet<T>();
		    }
		
		    for (String item : collection) {
		    	
		    	if (item.toString().startsWith("-")) {
		        	throw new IllegalArgumentException("- prefix has been deprecated for list-like properties! Full overriding happens (see: http://p6spy.github.io/p6spy/2.0/configandusage.html)");
		        }
		    	
		        newValue.add((T) parse(type, item));
		    }
		    map.put(key, newValue);	    
		}
	    
	    // propagate the changes
	    fireOptionChanged(key, oldValue, newValue);

	    return true;
	  }

	  void fireOptionChanged(final String key, final Object oldValue, final Object newValue) {
	    if (initCompleted) {
	      fireDelayedOptionChanges();
	      for (PermsOptionChangedListener listener : listeners) {
	        listener.optionChanged(key, oldValue, newValue);
	      }
	    } else {
	      delayedOptionChanges.add(new DelayedOptionChange(key, oldValue, newValue));
	    }
	  }

	  private synchronized void fireDelayedOptionChanges() {
	    if (null == delayedOptionChanges) {
	      return;
	    }

	    for (DelayedOptionChange delayedOption : delayedOptionChanges) {
	      for (PermsOptionChangedListener listener : listeners) {
	        listener.optionChanged(delayedOption.getKey(), delayedOption.getOldValue(),
	            delayedOption.getNewValue());
	      }
	    }

	    // make sure the delayed options get cleared to continue normal processing
	    delayedOptionChanges = null;
	  }

	  @SuppressWarnings("unchecked")
	  public <T> T get(Class<T> type, String key) {
	    if (!initCompleted) {
	      throw new IllegalStateException("Options didn't load completely, yet!");
	    }
	    return (T) map.get(key);
	  }

	  @SuppressWarnings("unchecked")
	  public <T> Set<T> getSet(Class<T> type, String key) {
	    return (Set<T>) map.get(key);
	  }

	  public void registerOptionChangedListener(PermsOptionChangedListener listener) {
	    if (listener == null) {
	      throw new IllegalArgumentException("PermsOptionChangedListener can't be null!");
	    }

	    this.listeners.add(listener);
	  }

	  public void unregisterOptionChangedListener(PermsOptionChangedListener listener) {
	    if (listener == null) {
	      throw new IllegalArgumentException("PermsOptionChangedListener can't be null!");
	    }

	    this.listeners.remove(listener);
	  }

	  class DelayedOptionChange {

		private final String key;
	    private final Object oldValue;
	    private final Object newValue;

	    public DelayedOptionChange(String key, Object oldValue, Object newValue) {
	      super();
	      
	      if (null == key || key.isEmpty()) {
	    	  throw new IllegalArgumentException("key can be neither null nor empty!");
	      }
	      
	      this.key = key;
	      this.oldValue = oldValue;
	      this.newValue = newValue;
	    }

	    
	    @Override
	    public int hashCode() {
	      // this is important part!
	      return key.hashCode();
	    }
	    
	    @Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			DelayedOptionChange other = (DelayedOptionChange) obj;
			if (!getOuterType().equals(other.getOuterType()))
				return false;
			if (key == null) {
				if (other.key != null)
					return false;
			} else if (!key.equals(other.key))
				return false;
			return true;
		}
	    
	    public String getKey() {
	      return key;
	    }

	    public Object getOldValue() {
	      return oldValue;
	    }

	    public Object getNewValue() {
	      return newValue;
	    }

		private P6OptionsRepository getOuterType() {
			return P6OptionsRepository.this;
		}
	  }
	}

}
